import { render, screen, fireEvent, waitFor, cleanup, } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import QuestionDetailClient from "./question-detail-client"; import { useCattellQuestionsQuery } from "@/hooks/marriage/use-cattell"; import { useGlasserQuestionsQuery } from "@/hooks/marriage/use-glasser"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useFormOverviewQuery, useFormSectionQuery, } from "@/hooks/marriage/use-form-schema"; import { convertOverviewToFrontendItems, mapBackendSectionToFrontend, } from "@/lib/schema-adapter"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; vi.mock("next/navigation", () => ({ useRouter: vi.fn(() => ({ replace: vi.fn(), push: vi.fn() })), })); vi.mock("@/translations/provider", () => ({ useI18n: vi.fn(() => ({ locale: "en", dictionary: new Proxy({}, { get: (_, key) => key }), })), })); vi.mock("@/hooks/marriage/use-cattell", () => ({ useCattellQuestionsQuery: vi.fn(), useSubmitCattellAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })), })); vi.mock("@/hooks/marriage/use-glasser", () => ({ useGlasserQuestionsQuery: vi.fn(), useSubmitGlasserAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })), })); vi.mock("@/hooks/marriage/use-profile-main", () => ({ useMarriageProfileQuery: vi.fn(), })); vi.mock("@/hooks/marriage/use-form-schema", () => ({ useFormOverviewQuery: vi.fn(), useFormSectionQuery: vi.fn(), })); vi.mock("@/hooks/marriage/use-habcoin-payment", () => ({ useHabcoinPaymentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })), })); vi.mock("@/lib/schema-adapter", () => ({ convertOverviewToFrontendItems: vi.fn(), mapBackendSectionToFrontend: vi.fn(), })); describe("QuestionDetailClient Validation", () => { const mockCattellRefetch = vi.fn(); const mockGlasserRefetch = vi.fn(); afterEach(() => { cleanup(); }); beforeEach(() => { vi.clearAllMocks(); (useMarriageProfileQuery as any).mockReturnValue({ data: { age: 30, gender: "male" }, isLoading: false, }); (useFormOverviewQuery as any).mockReturnValue({ data: {}, isLoading: false, }); (useFormSectionQuery as any).mockReturnValue({ data: undefined, isLoading: false, }); }); const setupTest = (slug: string, cattellData: any, glasserData: any) => { // Mock schema adapter to return an item for the requested slug (convertOverviewToFrontendItems as any).mockReturnValue([ { slug: slug, title: "Test", questions: [], }, ]); (useCattellQuestionsQuery as any).mockReturnValue({ data: cattellData, isLoading: false, isError: false, refetch: mockCattellRefetch, }); (useGlasserQuestionsQuery as any).mockReturnValue({ data: glasserData, isLoading: false, isError: false, refetch: mockGlasserRefetch, }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); render( , ); // Intro screen might render first; if Start is available, click it to mount questions flow const startButton = screen.queryByText("Start"); if (startButton) { fireEvent.click(startButton); } }; it("should render correctly when Cattell API data is completely valid", () => { setupTest( "personality_test", { questions: [ { question_number: 1, text: "Valid Question Cattell", options: [ { id: "opt_a", label: "Opt1", value: "A" }, { id: "opt_b", label: "Opt2", value: "B" }, { id: "opt_c", label: "Opt3", value: "C" }, ], }, ], }, null, ); // Retry UI should NOT be present expect(screen.queryByText("Retry")).toBeNull(); // Question text should be visible expect(screen.getByText("Valid Question Cattell")).toBeDefined(); }); it("should render Retry UI when Cattell API data is empty", () => { setupTest("personality_test", { questions: [] }, null); expect( screen.getAllByText("No questions found for this test."), ).toBeDefined(); expect(screen.getAllByText("Retry")).toBeDefined(); }); it("should render Retry UI when Cattell options are invalid (schema failure) and trigger refetch on Retry", async () => { setupTest( "personality_test", { questions: [ { question_number: 1, text: "Invalid Question", options: [{ label: "Opt1", value: "A" }], // Invalid schema }, ], }, null, ); expect( screen.getAllByText("No questions found for this test."), ).toBeDefined(); const retryBtn = screen.getAllByText("Retry")[0]; fireEvent.click(retryBtn); await waitFor(() => { expect(mockCattellRefetch).toHaveBeenCalled(); }); }); it("should render correctly when Glasser API data is completely valid", () => { setupTest("glasser_5_needs_test", null, { questions: [ { question_number: 1, text: "Valid Question Glasser", factor_code: "SUR", options: [ { id: "o1", label: "O1", value: 1 }, { id: "o2", label: "O2", value: 2 }, { id: "o3", label: "O3", value: 3 }, { id: "o4", label: "O4", value: 4 }, { id: "o5", label: "O5", value: 5 }, ], }, ], }); expect(screen.queryByText("Retry")).toBeNull(); expect(screen.getByText("Valid Question Glasser")).toBeDefined(); }); it("should render Retry UI when Glasser options are invalid (schema failure) and trigger refetch on Retry", async () => { setupTest("glasser_5_needs_test", null, { questions: [ { question_number: 1, text: "Invalid Question Glasser", factor_code: "SUR", options: [ { label: "O1", value: 1 }, { label: "O2", value: 2 }, { label: "O3", value: 3 }, { label: "O4", value: 4 }, ], }, ], }); expect( screen.getAllByText("No questions found for this test."), ).toBeDefined(); const retryBtn = screen.getAllByText("Retry")[0]; fireEvent.click(retryBtn); await waitFor(() => { expect(mockGlasserRefetch).toHaveBeenCalled(); }); }); it("should render profile questions using ID-based data flow", () => { const profileItem = { slug: "profile_test", title: "Profile Form", questions: [ { id: "q_123", title: "Dynamic ID Question", type: "text", order: 1, required: true, isVisible: true, private: false, description: "", tooltip: "", extras: {}, options: [], }, { id: "q_456", title: "Another ID Question", type: "radio", order: 2, required: false, isVisible: true, private: false, description: "", tooltip: "", extras: {}, options: [ { id: "opt_1", label: "Yes", value: "yes", order: 1 }, { id: "opt_2", label: "No", value: "no", order: 2 }, ], }, ], }; (convertOverviewToFrontendItems as any).mockReturnValue([ { slug: "profile_test", title: "Profile Form", questions: [], }, ]); (useFormSectionQuery as any).mockReturnValue({ data: { section: { cards: [] }, answers: {}, section_progress: { completion_percent: 0 }, }, isLoading: false, }); (mapBackendSectionToFrontend as any).mockReturnValue(profileItem); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); render( , ); // Profile questions render directly, no start button expect(screen.getByText(/Dynamic ID/)).toBeDefined(); }); it("renders a stable profile loading shell while section data is cold", () => { (convertOverviewToFrontendItems as any).mockReturnValue([ { slug: "profile_test", title: "Overview Profile Title", questions: [], }, ]); (useFormSectionQuery as any).mockReturnValue({ data: undefined, isLoading: true, }); (useCattellQuestionsQuery as any).mockReturnValue({ data: undefined, isLoading: false, }); (useGlasserQuestionsQuery as any).mockReturnValue({ data: undefined, isLoading: false, }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); render( , ); expect(screen.getByText("Overview Profile Title")).toBeDefined(); expect(screen.getByRole("status")).toBeDefined(); expect(screen.getByRole("button", { name: "Close" })).toBeDefined(); expect(screen.getByRole("button", { name: "Info" })).toBeDefined(); expect(screen.getByRole("button", { name: "Continue" })).toBeDisabled(); expect(document.querySelector(".shimmer-bg")).toBeNull(); }); it("renders cached section questions while the overview refreshes", () => { const cachedItem = { slug: "profile_test", title: "Cached Profile Form", questions: [ { id: "cached_question", title: "Cached question", type: "text", order: 1, required: true, isVisible: true, private: false, description: "", tooltip: "", extras: { placeHolder: "", range: [0, 0], options: [] }, options: [], }, ], }; (convertOverviewToFrontendItems as any).mockReturnValue([]); (useFormOverviewQuery as any).mockReturnValue({ data: undefined, isLoading: true, }); (useFormSectionQuery as any).mockReturnValue({ data: { section: { cards: [] }, answers: {}, section_progress: { completion_percent: 0 }, }, isLoading: false, }); (mapBackendSectionToFrontend as any).mockReturnValue(cachedItem); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); render( , ); expect(screen.getByRole("textbox")).toBeDefined(); expect(screen.queryByRole("status")).toBeNull(); }); it("uses the route title when the overview is also cold", () => { (convertOverviewToFrontendItems as any).mockReturnValue([]); (useFormOverviewQuery as any).mockReturnValue({ data: undefined, isLoading: true, }); (useFormSectionQuery as any).mockReturnValue({ data: undefined, isLoading: true, }); (useCattellQuestionsQuery as any).mockReturnValue({ data: undefined, isLoading: false, }); (useGlasserQuestionsQuery as any).mockReturnValue({ data: undefined, isLoading: false, }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); render( , ); expect(screen.getByText("Route Fallback Title")).toBeDefined(); expect(screen.getByRole("status")).toBeDefined(); expect(document.querySelector(".shimmer-bg")).toBeNull(); }); });